import { sRand, getBounds, isAlongAxis, getClampedRects } from './raw/math'
import { fixRayLoops } from './raw/rayLoopsWithHoles'
import { handleBoundingBox, handleSlopingRoof, handleAppendent } from './raw/handleOthers'
import { handleVertical } from './raw/handleVertical'
import { handleHorizontal } from './raw/handleHorizontal'
import { handleMatch } from './raw/handleMatch'
import { handleExtrude } from './raw/handleExtrude'
import {
  offsetRayLoops,
  rectClampRayLoops,
  indentRayLoops,
  splitRayloops,
  XYFromRayLoops,
} from './raw/rayLoops'
import { generate } from './model/generate'
import { MagizStyles } from '../styleClass'
import { Vector2, getArea, isClockWise } from './raw/_imports'

import type { MagizTypes, MagizService, StyleParsed, Temp } from './raw/_imports'

export { Plan }

/** 建筑平面类，包括用于生成模型的相关数据和方法 */
class Plan {
  /** 输入的源坐标，第一个逆时针为外圈，后续为顺时针内圈。顺逆关系不能改变！ */
  orignal: MagizTypes.XY[][]
  /** 原平面的中心点坐标，用于移动模型到原位 */
  center: MagizTypes.XY
  /** 平面的面积 */
  area: number
  /** 平面的中心点对齐原点并将长边对齐轴线后的相对平面（用于简化计算），引入 Temp.Ray 以直接用于生成计算 */
  relative: {
    /** 相对平面定界框的最小点和最大点 */
    bounds: { min: MagizTypes.XY; max: MagizTypes.XY }
    /** bounds 的开间与进深，用于计算按比例缩进的距离 */
    size: { x: number; y: number }
    /** 边线转为计算用的向量数据 */
    rayLoops: Temp.Ray[][]
    /** 围绕坐标轴原点旋转到原位的弧度 */
    radian: number
  }

  /** 建筑平面。坐标数据将中心点设为原点并将长轴对齐X轴以简化计算。 */
  constructor(loops: MagizService.SingleRequest['loops']) {
    // 统一格式化为 XY 的副本
    const loopsXY = loops.map((loop) =>
      loop.map((pt) => (Array.isArray(pt) ? { x: pt[0], y: pt[1] } : pt))
    )

    // 清理首尾相同的点，避免 ThreeJS 生成的面缺角
    loopsXY.forEach((loop) => {
      const first = loop[0]!
      const last = loop[loop.length - 1]!
      if (first.x === last.x && first.y === last.y) loop.pop()
    })

    // THreeJS 在创建多边形时都会检查clockwise，但为了保证后续的数学计算正确须提前格式化
    this.orignal = []
    for (let i = 0; i < loopsXY.length; i++) {
      const loop = loopsXY[i]!
      if (i > 0) {
        // 内圈顺时针
        if (!isClockWise(loop)) loop.reverse()
      } else {
        // 外圈逆时针
        if (isClockWise(loop)) loop.reverse()
      }
      this.orignal.push(loop)
    }

    const outter = this.orignal[0]

    if (outter && getBounds(outter)) {
      // 计算面积
      this.area = getArea(outter)
      for (let i = 1; i < this.orignal.length; i++) {
        this.area -= getArea(this.orignal[i]!)
      }

      // 计算外边线的矢量，寻找外边线最长的那段
      const longest = new Vector2()
      outter.forEach((current, i) => {
        const next = outter[i + 1 === outter.length ? 0 : i + 1]!
        const v = new Vector2().subVectors(next, current)
        if (v.length() > longest.length()) longest.copy(v)
      })

      const origin = new Vector2()
      const radian = longest.angle()
      this.center = getCenter(outter)

      // 将坐标点的中心重置到原点
      const relativePoints = this.orignal.map((loop) =>
        loop.map((v2) => new Vector2().subVectors(v2, this.center).rotateAround(origin, -radian))
      )
      const bounds = getBounds(relativePoints[0])!

      this.relative = {
        radian,
        bounds,
        size: {
          x: bounds.max.x - bounds.min.x,
          y: bounds.max.y - bounds.min.y,
        },
        rayLoops: relativePoints.map((loop) =>
          loop.map((start, i) => {
            const end = loop[i + 1 === loop.length ? 0 : i + 1]!
            const direction = end.clone().sub(start)
            return { start, end, direction }
          })
        ),
      }
    } else {
      throw 'ERROR: invalid input.loops'
    }
  }

  /** 按平面和样式生成模型的JSON数据。生成多个建筑时若全部以原点为中心，origin须设为全部建筑的中心点 */
  toRaw(
    styles: MagizStyles,
    styleParams: MagizTypes.StyleParams,
    origin?: MagizTypes.XY
  ): MagizTypes.DataJSON.ModelData {
    // 计算生成的原点
    if (!origin) origin = { x: 0, y: 0 }
    const centerRelative: MagizTypes.XY = {
      x: this.center.x - origin.x,
      y: this.center.y - origin.y,
    }

    // 将结果保存到公共变量，以便同时处理多个plan生成，以及每个平面都正确映射colorMap
    const { colors, floorCount, classified } = styles.parseStyle(styleParams)

    const result: MagizTypes.DataJSON.ModelData = {
      styleParams,
      info: { floorArea: this.area, floors: floorCount },
      points: XYFromRayLoops(this.relative.rayLoops),
      center: this.center,
      centerRelative,
      rotate: this.relative.radian,

      instanced: {
        box: { matrices: [], colors: [] },
        boxGlass: { matrices: [], colors: [] },
        slope2: { matrices: [], colors: [] },
        slope2Glass: { matrices: [], colors: [] },
        slope4: { matrices: [], colors: [] },
        slope4Glass: { matrices: [], colors: [] },
      },
      extruded: { solid: [], glass: [] },

      colors,
    }

    // 解析样式时按参数的JSON进行分类，相同的仅计算一次
    for (const edgeParamsJSON in classified) {
      const { params, parsed } = classified[edgeParamsJSON]!

      /** 根据边线控制参数处理后的 rayLoops */
      const rayLoops = planToRelativeRayLoops(this, params)
      /** 修正后的 rayLoops，用于 extrude 和 match */
      let rayLoopsFixed: Temp.Ray[][] = []

      parsed.forEach((dataParsed) => {
        if (
          (dataParsed.extrude.length > 0 || dataParsed.match.length > 0) &&
          rayLoopsFixed.length === 0
        ) {
          rayLoopsFixed = fixRayLoops(rayLoops)
        }
        handleExtrude(result, dataParsed, rayLoopsFixed, Math.abs(Number(styleParams.match) || 0))
        handleMatch(result, dataParsed, rayLoopsFixed)

        handleVertical(result, dataParsed, rayLoops)
        handleHorizontal(result, dataParsed, rayLoops)
        handleAppendent(result, dataParsed, rayLoops)

        const outter = rayLoops[0]
        const bounds = getBounds(outter?.map((r) => r.start))
        if (outter && bounds) {
          handleBoundingBox(result, dataParsed, bounds)
          handleSlopingRoof(result, dataParsed, bounds)
        }
      })
    }

    return result
  }
  toModel(
    styles: MagizStyles,
    styleParams: MagizTypes.StyleParams,
    params?: Partial<MagizTypes.GenerateOptions> & {
      /** 生成的模型将以此为中心 */
      origin?: MagizTypes.XY
    }
  ) {
    return generate(this.toRaw(styles, styleParams, params?.origin), params)
  }
}

function getCenter(loop: MagizTypes.XY[]): MagizTypes.XY {
  const c: MagizTypes.XY = { x: 0, y: 0 }
  loop.forEach((v) => {
    c.x += v.x
    c.y += v.y
  })
  c.x /= loop.length
  c.y /= loop.length
  return c
}

/** 根据样式参数中的 setEdges 处理边线并生成新的向量数组 */
function planToRelativeRayLoops(plan: Plan, edgeParams: StyleParsed.HandleEdge[]): Temp.Ray[][] {
  const { rayLoops, bounds, size } = plan.relative
  const outter = rayLoops[0]!
  let result = rayLoops
  // 按顺序多次修正边线
  edgeParams.forEach((params) => {
    // const { offset, along, clamp, indent, split } = params
    if ('offset' in params) {
      result = offsetRayLoops(rayLoops, size, params.offset)
    } else if ('clamp' in params) {
      const rects = getClampedRects(bounds, params.clamp)
      result = rectClampRayLoops(rayLoops, rects)
    } else if ('indent' in params) {
      result = indentRayLoops(rayLoops, params.indent)
    } else if ('split' in params) {
      result = splitRayloops(rayLoops, params.split)
    } else if ('along' in params) {
      switch (params.along) {
        case 'WIDTH':
          result = rayLoops
            .map((loop) => loop.filter((ray) => isAlongAxis(ray.direction.angle())))
            .filter((loop) => loop.length > 0)
          break
        case 'DEPTH':
          result = rayLoops
            .map((loop) => loop.filter((ray) => !isAlongAxis(ray.direction.angle())))
            .filter((loop) => loop.length > 0)
          break
        case 'RANDOM':
          result = rayLoops
            .map((loop) => loop.filter(() => sRand() < 1))
            .filter((loop) => loop.length > 0)
          break
        case 'SHORTEST':
        case 'LONGEST':
          {
            // 找到最长或最短的射线
            const { direction } =
              params.along === 'LONGEST'
                ? outter.reduce((a, b) => (a.direction.length() > b.direction.length() ? a : b))
                : outter.reduce((a, b) => (a.direction.length() < b.direction.length() ? a : b))
            result = rayLoops.map((loop) =>
              loop.filter((ray) => isAlongAxis(ray.direction.angleTo(direction)))
            )
          }
          break
        default:
          const radianAlongAxisX = params.along
          // 默认按与世界坐标轴的角度筛选边线
          result = rayLoops
            .map((loop) =>
              loop.filter(
                (ray) => Math.abs(ray.direction.angle() - radianAlongAxisX) < 10 * (Math.PI / 180)
              )
            )
            .filter((loop) => loop.length > 0)
          break
      }
    }
  })

  return result
}
